package se.webbzon.oschi01.json;

import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.io.Reader;

public class JsonInputStreamReader extends JsonReader<InputStream> {
	
	private static final char[] cNull = new char[] {'u', 'l', 'l'};
	private static final char[] cTrue = new char[] {'r', 'u', 'e'};
	private static final char[] cFalse = new char[] {'a', 'l', 's', 'e'};
	
	public JsonInputStreamReader() {
		this(null);
	}
	
	public JsonInputStreamReader(InputStream in) {
		super(in);
	}

	@Override public void close() throws IOException {
		synchronized (lock) {
			if (lock == null)
				throw new IOException("No input stream was provided for this reader.");
			((InputStream) lock).close();
		}
	}

	@Override public JsonType read() throws IOException {
		synchronized (lock) {
			if (lock == null)
				throw new IOException("No input stream was provided for this reader.");
			return read((InputStream) lock);
		}
	}
	
	@Override public JsonType read(InputStream is) throws IOException {
		InputStreamReader reader = new InputStreamReader(is);
		return read(reader, new NextCharacter(reader.read()));
	}
	
	private static JsonType read(Reader reader, NextCharacter next) throws IOException {
		// Ignore whitespace
		int i = next.c;
		while (Character.isWhitespace(i)) {
			i = reader.read();
		}
		next.c = i;
		if (i == -1)
			return null;
		
		// Switch by first letter
		char c = (char) i;
		next.c = reader.read();
		switch (c) {
		case '-':
			return readNumber(reader,next,'0',true);
		case '\"':
		case '\'':
			return readString(reader,next,c);
		case 'f':
			return readBoolean(reader,next,false);
		case 't':
			return readBoolean(reader,next,true);
		case 'n':
			return readerNull(reader,next);
		case '{':
			return readObject(reader,next);
		case '[':
			return readArray(reader,next);
		case '}':
		case ']':
		case ',':
		case ':':
			return new JsonSeparator("" + c);
		default:
			if (isDigit(c))
				return readNumber(reader,next,c,false);
			else
				throw new IOException("Unexpected syntax." + c + ":" + next.c);
		}
	}
	
	private static JsonNull readerNull(Reader reader, NextCharacter next) throws IOException {
		for (int j = 0; j < cNull.length; j++) {
			int i = next.c;
			if (i == -1)
				return null;
			else if (cNull[j] == (char) i) {
				next.c = reader.read();
			} else
				throw new IOException("Unexpected syntax.");
		}
		return new JsonNull();
	}
	
	private static JsonBoolean readBoolean(Reader reader, NextCharacter next, boolean b) throws IOException {
		char[] seq = b ? cTrue : cFalse;
		for (int j = 0; j < seq.length; j++) {
			int i = next.c;
			if (i == -1)
				return null;
			else if (seq[j] == (char) i) {
				next.c = reader.read();
			} else
				throw new IOException("Unexpected syntax.");
		}
		return new JsonBoolean(b);
	}
	
	private static JsonString readString(Reader reader, NextCharacter next, char escape) throws IOException, EOFException {
		StringBuilder builder = new StringBuilder();
		boolean special = false;
		while (true) {
			int j = next.c;
			if (j == -1)
				return null;
			next.c = reader.read();
			char d = (char) j;
			if (special) {
				builder.append(readChar(reader,next,-1)); 
			} else if (d == '\\') {
				special = true;
			} else if (d == escape) {
				return new JsonString(builder.toString());
			} else {
				builder.append(d);
			}
		}
	}
	
	private static int readChar(Reader reader, NextCharacter next, int remaining) throws IOException {
		int i = next.c;
		if (i == -1)
			return -1;
		
		next.c = reader.read();
		char c = (char) i;
		if (remaining == -1) {
			switch (c) {
			case 'u':
				return readChar(reader,next,3);
			case 'b':
				return '\b';
			case 'f':
				return '\f';
			case 'n':
				return '\n';
			case 'r':
				return '\r';
			case 't':
				return '\t';
			default:
				return c;
			}
		} else {
			final char d;
			if (c >= 48 && c < 58)
				d = (char) (c - 48);
			else if (c >= 65 && c < 71)
				d = (char) (c - 55);
			else
				d = (char) (c - 87);
			if (remaining == 0)
				return d;
			else {
				int j = readChar(reader,next,remaining-1);
				return j == -1 ? -1 : (char) (d * 16 + j);
			}
		}
	}
	
	private static JsonObject readObject(Reader reader, NextCharacter next) throws IOException {
		JsonObject object = new JsonObject();
		JsonType key = read(reader,next);
		while (true) {
			if (!(key instanceof JsonString))
				throw new IOException("Key to JSON object must be a string.");
			
			JsonType sep = read(reader,next); // Skip ":"
			if (!sep.toString().equals(":"))
				throw new IOException("Unexpected syntax.");
			
			JsonType value = read(reader,next);
			if (value == null)
				throw new IOException("Unexpected syntax.");
			else
				object.put((JsonString) key, value);
			
			sep = read(reader,next); // Skip "," and abort on "}"
			if (sep.toString().equals("}"))
				return object;
			else if (!sep.toString().equals(","))
				throw new IOException("Unexpected syntax.");
			
			key = read(reader,next);
		}
	}
	
	private static JsonArray readArray(Reader reader, NextCharacter next) throws IOException, EOFException {
		JsonArray array = new JsonArray();
		JsonType type = read(reader,next);
		while (true) {
			if (type.toString().equals("]"))
				return array;
			else if (!type.toString().equals(","))
				array.add(type);
			type = read(reader,next);
		}
	}
	
	private static JsonNumber readNumber(Reader reader, NextCharacter next, char n, boolean negate) throws IOException {
		StringBuilder builder = new StringBuilder("" + n);
		while (true) {
			int i = next.c;
			if (i == -1)
				return null;
			if (inNumber(i)) {
				builder.append((char) i);
				next.c = reader.read();
			} else try {
				double d = Double.parseDouble(builder.toString());
				return new JsonNumber(negate ? -d : d);
			} catch (NumberFormatException e) {
				throw new IOException(e);
			}
		}
	}
	
	private static final boolean isDigit(int c) {
		return (c >= '0' && c <= '9');
	}
	
	private static final boolean inNumber(int c) {
		return (c >= '0' && c <= '9') || c == '-' || c == '.' || c == '+' || c == 'e' || c == 'E';
	}
	
	private static final class NextCharacter {
		
		int c;
		
		public NextCharacter(int c) {
			this.c = c;
		}
		
	}
	
	private static final class JsonSeparator implements JsonType {
		
		/**
		 * 
		 */
		private static final long serialVersionUID = 3288176023575522598L;
		String separator;
		
		public JsonSeparator(String separator) {
			this.separator = separator;
		}

		@Override public Type getType() {
			return JsonType.Type.String;
		}

		@Override public JsonSeparator clone() {
			return new JsonSeparator(separator);
		}
		
		@Override public String toString() {
			return separator;
		}

		@Override public void printTo(PrintStream ps) throws IOException {
			ps.print(toString());
		}
		
	}

}
